import os, os.path, pickle, time, copy
pj = os.path.join
from pydispatch import dispatcher
import osg2

import lucid.util.plugin
import lucid.util.mixins
import lucid.view
#from lucid.core import signal
import lucid.core


class Scene(object):
   """ A scene in the system. 
   @var mName: Name of the scene
   @var mFileName: If set, this is the filename to save to or load from.
   @var mSceneRoot: OSG scene root node.
   @var mFcTypeMap: Map to all found FC's by their typenames to a set() of fcptrs
   @var mPool: ContainerCollection to store fc's that we are editing but are not in graph.
   """
   def __init__(self, rootNode, name="", filename=""):
      self.mFileName = filename 
      self.mName = name
      self.mMaterialList = []
      self.mFcTypeMap = {}
      self.mPool = None
      self.mLightDecorator = LightDecorator(self)

      self.mSceneRoot = rootNode      
      
      if self.mSceneRoot:    
         self.initializePool()
         self.updateTypeMap()    
      if not self.mName and self.mFileName:
         self.mName = os.path.basename(self.mFileName)      
   
   def save(self):
      """ Save scene out to file. """
      if not self.mFileName or self.mFileName == "":
         raise IOError, "Scene has no filename"
      else:
         osg2.SceneFileHandler.the().write(self.mSceneRoot, str(self.mFileName))
   
   def initializePool(self):
      """ Initialize the pool object. """
      print "Initializing pool...",
      if not self.mSceneRoot:
         return 
      
      # Look for pool object named "lucid_pool" off root
      # If not there, create it
      cp = None
      cur_index = 0
      while None == cp:
         attachment = self.mSceneRoot.findAttachment(osg2.ContainerCollection.getClassGroupId(),
                                                     cur_index)
         if not attachment:
            break   # Exit with an place to put in pool
         elif (type(attachment) == osg2.ContainerCollection) and \
              (attachment.getName() == "lucid_pool"):
            cp = attachment
            print "found at index: ", cur_index
            break
         cur_index += 1
      
      # Should have either None or a valid pool
      if cp:
         assert cp.getName() == "lucid_pool"
         self.mPool = cp
      else:
         print "created at index: ", cur_index
         cp = osg2.ContainerCollection.create()
         cp.setName("lucid_pool")
         self.mSceneRoot.addAttachment(cp,cur_index)
         self.mPool = cp

   def addToPool(self, containers):
      """ Add a list of containers to pool. 
          Emits PoolChanged signal and triggers typemap update.
      """
      if not self.mPool:
         return
      
      if not type(containers) == list:
         containers = [containers]

      #existing_containers = self.mPool.getContainers()
      #added_containers = [x for x in containers if x not in existing_containers]
      print "Adding to pool: XXX: Fix getContainers"
      
      added_containers = containers      
      for c in added_containers:
         self.mPool.addContainer(c)
      print "New pool size: ", self.mPool.getNContainers()
      
      # Signal everyone that the pool has changed.
      # - We will explicitly update the type map which may end up sending another signal
      dispatcher.send(lucid.core.signal.PoolChanged, added=added_containers, removed=[])
      self.addFieldContainersToTypeMap(added_containers)


   def removeFromPool(self, containers):
      pass

   def updateTypeMap(self):
      print "Starting type map update..."
      start_time = time.time()      
      self.mFcTypeMap = {}
      self.mFcTypeMap["Node"] = set()
      if self.mSceneRoot:
         osg2.traverse(self.mSceneRoot, self._fc_type_map_trav_Cb)
      if self.mPool:
         self.updateTypeMapFromFieldContainer(self.mPool)
      print "Update typemap took: %s seconds"%(time.time()-start_time)
      dispatcher.send(lucid.core.signal.TypeMapChanged)
      
   def _fc_type_map_trav_Cb(self, node):
      self.mFcTypeMap["Node"].add(node)
      print ".",
      core = node.getCore()
      if core:
         self.updateTypeMapFromFieldContainer(core, False)

      return osg2.Action.Continue

   def updateTypeMapFromFieldContainer(self, fc, emitUpdateSignal=True):
      """ Update the typemap with all entries found in the given fc. 
          @param emitUpdateSignal: If true and changes happen, emit signal.
      """
       # Add the fc to the map
      fc_type = fc.getType()
      self.addFieldContainersToTypeMap(fc, emitUpdateSignal)      
      
      # Now pull all related ptrs
      # - Get the field names
      ptr_field_names = []      
      for fd in [fc_type.getFieldDesc(x) for x in range(1,fc_type.getNumFieldDescs()+1)]:
         ft_name = fd.getFieldType().getCName()
         if ft_name.endswith("Ptr") and \
            (ft_name.startswith("SF") or ft_name.startswith("MF")):            
               ptr_field_names.append(fd.getCName())
      
      #print "Field names: ", ptr_field_names
      
      # - Now get the pointers from sf and mf fields using getters
      found_ptrs = []
      for n in ptr_field_names:
         getter_name = "get%s%s"%(n[0].upper(),n[1:])
         if getter_name in ["getParents"]:     # XXX: HACK: List of getters to skip.
            continue
         getter = getattr(fc,getter_name,None)
         
         # XXX: Clean this up.  It fails in some cases
         #    with certain return types (FieldContainerPtr's)
         try:
            if getter:
               ptrs = getter()
               #print "  getter: [%s]  returned: %s"%(getter_name, ptrs)
               if ptrs:
                  if type(ptrs) == list:          # mf case
                     found_ptrs.extend([p for p in ptrs if p])
                  else:                           # sf case
                     found_ptrs.append(ptrs)
         except:
            pass
      
      # - Add pointers to map/set
      self.addFieldContainersToTypeMap(found_ptrs, emitUpdateSignal)

   def addFieldContainersToTypeMap(self, fcPtrs, emitUpdateSignal=True):
      """ Add all fc in the list to the typemap. 
          @param emitUpdateSignal: If true, emit TyepMapChanged if it changes.
      """
      if type(fcPtrs) != list:
         fcPtrs = [fcPtrs]
      
      # List of ptrs to "new" fc's that we need to recursively look at
      recurse_ptrs = []
      
      # Add pointers to map/set
      for p in fcPtrs:
         try:
            type_name = p.getType().getCName()
            if not self.mFcTypeMap.has_key(type_name):
               self.mFcTypeMap[type_name] = set()
            if not p in self.mFcTypeMap[type_name]:
               self.mFcTypeMap[type_name].add(p)
               recurse_ptrs.append(p)
         except:
            print "Failed to get information for ptr: ", p
      
      # Recurse into the new ptrs we just found that are new
      for fc in recurse_ptrs:
         self.updateTypeMapFromFieldContainer(fc, False)
      
      # Send signal if necessary.
      if emitUpdateSignal and (len(fcPtrs) > 0):
         dispatcher.send(lucid.core.signal.TypeMapChanged)


class LightDecorator(object):
   """ Decorates all lights in graph by attaching geometry under
       their beacons.
   """
   def __init__(self, scene):      
      self.mScene = scene
      self.mDecoratedNodes = {}  # map: light_fc --> [light_decor_transform]
      
      light_model_name = pj(lucid.core.Environment().mDataDir, "light.osb")
      print "Loading: ", light_model_name
      self.mLightModel = osg2.SceneFileHandler.the().read(light_model_name)
      
      dispatcher.connect(self.onFCModified, lucid.core.signal.SelectedFieldContainerModified)
   
   def onFCModified(self):
      """ Called when the currently selected fc in the env is modified.
          If it is a light we are decorating, then we need to update it.
      """
      cur_fc = lucid.core.Environment().selectedFC
      if self.mDecoratedNodes.has_key(cur_fc):
         print "Updating light decoration for fc: ", cur_fc
         self.updateLightDecorator(cur_fc)
      
      
   def decorate(self):
      """ Decorate the scene with models to show all the lights. """
      root = self.mScene.mSceneRoot
      
      if root:
         osg2.traverse(self.mScene.mSceneRoot, self._decorate_cb)
         
         for l in self.mDecoratedNodes.keys():
            self.updateLightDecorator(l)
   
   def _decorate_cb(self, node):
      core = node.getCore()
      if not core or type(core) not in [osg2.DirectionalLight, osg2.PointLight, osg2.SpotLight]:
         return

      light_core = core
      if not self.mDecoratedNodes.has_key(light_core):
         # Setup the light geom and transform
         light_decor_transform = osg2.TransformNode.create()
         light_geom_node = osg2.cloneTree(self.mLightModel)
         light_decor_transform.node().addChild(light_geom_node)
         osg2.setName(light_decor_transform.node(), "LightDecorTransform")
         osg2.setName(light_geom_node, "LightDecorGeom")
         
         beacon_node = light_core.getBeacon()
         if beacon_node:
            beacon_node.addChild(light_decor_transform.node())
            self.mDecoratedNodes[light_core] = light_decor_transform
         else:
            print "Found light with no beacon, skipping it."


   def undecorate(self):
      """ Remove all decorations we have added to the scene. """
      for (light, decor_node) in self.mDecoratedNodes.iteritems():
         parent = decor_node.node().getParent()
         parent.subChild(decor_node.node())      
      self.mDecoratedNodes.clear()

   
   def updateLightDecorator(self, light):
      decor_xform = self.mDecoratedNodes[light]
      
      position = osg2.Vec3f(0,0,0)
      direction = osg2.Vec3f(0,0,-1)
      
      if hasattr(light,"getPosition"):
         position = light.getPosition()
      if hasattr(light,"getDirection"):
         direction = light.getDirection()
      
      mat = self.buildLightDirMatrix(position,direction)
      decor_xform.setMatrix(mat)
      
      
   def buildLightDirMatrix(self, position, direction):
      """ Return a matrix for transforming into light direction. """
      view = osg2.Vec3f(direction[0], direction[1], direction[2])
      up = osg2.Vec3f(0,1,0)
      
      view.normalize()
      right = up.cross(view)
      
      # Handle case of view straight up or straight down
      if right.dot(right)<0.01:
         up = osg2.Vec3f(1,0,0)
         right = up.cross(view)
      
      right.normalize()

      newup = view.cross(right)

      result = osg2.Matrix()
      result.setIdentity()
      result.setTranslate(position);

      tmpm = osg2.Matrix()
      tmpm.setValue(right, newup, view);

      result.mult(tmpm)
      return result
   
   
   
